/*   Copyright (C) 2008  Jeroen v. Schagen & Youri Lima
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

using System;

using Engine.Math;

namespace Engine
{
    public delegate void ComponentEventHandler(object sender);

    /// <summary>
    ///     Moveable object in a three-dimensional space.
    /// </summary>
    public class Component : Composite, IProcessable, IVisitable
    {
        public event ComponentEventHandler DataModified;

        // Identifier
        protected String name;

        // Mathematical attributes
        protected Quaternion orientation;
        protected Vector3 position;
        protected Vector3 scale;

        # region Constructor

        public void Accept(IVisitor visitor, Object arg)
        {
            visitor.Visit(this, arg);
        }

        /// <summary>
        ///     Construct an object using a certain name.
        /// </summary>
        /// 
        /// <param name="name">Name</param>
        public Component(String name) : this(name, null)
        {
            // Overloaded
        }

        /// <summary>
        ///     Construct an object using the given name and parent.
        /// </summary>
        /// 
        /// <param name="name">Name</param>
        /// <param name="parent">Parent</param>
        public Component(String name, Component parent)
        {
            // Store information
            this.name = name;
            this.parent = parent;

            // Initiate attributes
            orientation = Quaternion.IDENTITY;
            position = Vector3.ZERO;
            scale = Vector3.ONE;  
        }

        # endregion


        # region Function

        /// <summary>
        ///     Retrieve the direction of this object.
        /// </summary>
        public Vector3 Direction
        {
            get
            {
                if (parent != null)
                {
                    return GetParent().WorldOrientation * Vector3.UNIT_NEGATIVE_Z;
                }

                return WorldOrientation * Vector3.UNIT_NEGATIVE_Z; 
            }
        }

        /// <summary>
        ///     Place the object in such a way that it's direction
        ///     is directly facing the given position.
        /// </summary>
        public void LookAt(Vector3 position)
        {
            // Retrieve needed rotation
            Quaternion q = RotationTo(position);

            // Apply rotation to orientation
            if (parent != null) q = q * GetParent().WorldOrientation;
            else q = q * WorldOrientation;

            // Set orientation
            Orientation = q;
        }

        /// <summary>
        ///     Place the object in such a way that it's direction
        ///     is directly facing the given position.
        /// </summary>
        public void LookAt(float x, float y, float z)
        {
            LookAt(new Vector3(x, y, z));
        }

        /// <summary>
        ///     Notify this component and all it's children about
        ///     an event that occured.
        /// </summary>
        protected void NotifyObservers()
        {
            // Invoke data modified
            if (DataModified != null)
                DataModified(this);

            // Process children
            foreach (Component obj in children)
            {
                obj.NotifyObservers();
            }
        }

        /// <summary>
        ///     Process movable object over a certain time interval.
        /// </summary>
        public virtual void Process(IProcessor handler, TimeSpan time)
        {
            // Do nothing.
        }

        /// <summary>
        ///     Rotate over the z axis, using a certain degree angle.
        /// </summary>
        public void Roll(Degree degree)
        {
            Rotate(degree.ToRadian(), Vector3.UNIT_Z);
        }

        /// <summary>
        ///     Rotate over the z axis, using a certain degree angle.
        /// </summary>
        public void Roll(float value)
        {
            Rotate(new Radian(new Degree(value)), Vector3.UNIT_Z);
        }

        /// <summary>
        ///     Rotate over the x axis, using a certain degree angle.
        /// </summary>
        public void Pitch(Degree degree)
        {
            Rotate(degree.ToRadian(), Vector3.UNIT_X);
        }

        /// <summary>
        ///     Rotate over the x axis, using a certain degree angle.
        /// </summary>
        public void Pitch(float value)
        {
            Rotate(new Radian(new Degree(value)), Vector3.UNIT_X);
        }

        /// <summary>
        ///     Rotate the object, using the given quaternion.
        /// </summary>
        public void Rotate(Quaternion quaternion)
        {
            Orientation = this.orientation * quaternion;
        }

        /// <summary>
        ///     Rotate object, using a radian and  a certain axis.
        /// </summary>
        public void Rotate(Radian radian, Vector3 axis)
        {
            // Calculate orientation for angle
            Rotate(Quaternion.FromAngleAxis(radian, axis));
        }

        /// <summary>
        ///     Calculate needed quaternion, to orientate directly
        ///     towards the given position.
        /// </summary>
        public Quaternion RotationTo(Vector3 position)
        {
            // Calculate target direction
            Vector3 targetDirection = position - this.WorldPosition;
            targetDirection.Normalise();

            // Calculate needed rotation
            return Direction.Rotation(targetDirection);
        }

        /// <summary>
        ///     Translate the object over a given vector, while
        ///     staying relative to itself.
        /// </summary>
        public void Translate(Vector3 vector)
        {
            Position += (orientation * vector);
        }

        /// <summary>
        ///     Translate the object over a given vector, while
        ///     staying relative to itself.
        /// </summary>
        public void Translate(float x, float y, float z)
        {
            Translate(new Vector3(x, y, z));
        }

        /// <summary>
        ///     Calculate the world position of this object.
        /// </summary>
        public Vector3 WorldPosition
        {
            get
            {
                if (parent != null)
                {
                    // Change position vector based on parent's orientation and scale
                    Vector3 derived = GetParent().WorldScale * (GetParent().WorldOrientation * this.position);

                    // Add altered position vector to parents
                    return derived + GetParent().WorldPosition;
                }

                else return this.position;
            }
        }

        /// <summary>
        ///     Calculate the world orientation of this object.
        /// </summary>
        public Quaternion WorldOrientation
        {
            get
            {
                if (parent != null)
                {
                    // Multiply current orientation with parents orientation
                    return GetParent().WorldOrientation * orientation;
                }

                else return orientation;
            }
        }

        /// <summary>
        ///     Calculate the world scale of this object.
        /// </summary>
        public Vector3 WorldScale
        {
            get
            {
                if (parent != null)
                {
                    // Multiply scale with parents scale
                    return scale * GetParent().WorldScale;
                }

                else return scale;
            }
        }

        /// <summary>
        ///     Rotate over the y axis, using a certain degree angle.
        /// </summary>
        public void Yaw(Degree degree)
        {
            Rotate(degree.ToRadian(), Vector3.UNIT_Y);
        }

        /// <summary>
        ///     Rotate over the y axis, using a certain degree angle.
        /// </summary>
        public void Yaw(float value)
        {
            Rotate(new Radian(new Degree(value)), Vector3.UNIT_Y);
        }

        # endregion


        # region Accessor

        /// <summary>
        ///     Identifier of object.
        /// </summary>
        public String Name
        {
            get { return name; }
            set { name = value; }
        }

        /// <summary>
        ///     Orientation of object.
        /// </summary>
        public Quaternion Orientation
        {
            get { return orientation; }

            set
            {
                // Change orientation
                orientation = value;

                // Notify observers
                NotifyObservers();
            }
        }

        /// <summary>
        ///     Position of object.
        /// </summary>
        public Vector3 Position
        {
            get { return position; }

            set
            {
                // Change position
                position = value;

                // Notify observers
                NotifyObservers();
            }
        }

        /// <summary>
        ///     Scale of object.
        /// </summary>
        public Vector3 Scale
        {
            get { return scale; }

            set
            {
                // Change scale
                scale = value;

                // Notify observers
                NotifyObservers();
            }
        }

        public virtual Object GetAdapter(Type adapter)
        {
            if (adapter == typeof(Matrix4))
            {
                return Matrix4.FromTranslation(WorldPosition) * Matrix4.FromRotation(WorldOrientation);
            }

            return null;
        }

        /// <summary>
        ///     Retrieve objects parent, and convert it to a movable object.
        /// </summary>
        public new Component GetParent()
        {
            return (Component) base.GetParent();
        }

        # endregion

        public override string ToString()
        {
            return "Node: " + Name + ", P{" + Position.ToString() + "} WP{" + WorldPosition.ToString() + "}";
        }
    }
}
